Loops#
Spike detection with python#
Present data in code (individual voltage values, manipulate them and store the results) - variables
Compare variables (voltage to threshold) - boolean values
Perform different actions based on the value of a variable (only keep the position if the voltage exceeds the threshold) - if-else statements
Present and access data in a time series of voltage values - lists
Perform an action for each element in a sequence of values (inspect voltage values one-by-one) - for loops
Separate data and logic so we can use the same code for new recordings - functions
Apply this to multi data files
Plot and save the results
Warm-up#
To warm up, let’s do a little exercise.
Here is a list: my_list = [10, 20, 30, 40, 50]
Write code that divides each value in this list by 2 and prints the computed value.
my_list = [10, 20, 30, 40, 50]
# your solution here
Applying computations to list items using for-loops#
In the above code, the only thing that changes is the index.
We can re-write this much more succinctly using a for-loop:
my_list = [10, 20, 30, 40, 50]
indices = [0, 1, 2, 3, 4] # create a list with the indices of the items you want to work with
for index in indices: # "loop" over the list `indices`, one element at a time; at each step, 'index' holds the current number from 'indices'
# the indented block is what is execute at each step of the loop
item = my_list[index] # use 'index' to access the corresponding item from 'my_list'
print(item) # print the list item
10
20
30
40
50
What is going on here:
we define a variable called
my_list
as a list of 5 numbers: 10, 20, 30, 40, 50we define another variable called
indices
as a list of 5 numbers: 0, 1, 2, 3, 4. We want to use this list as indices to access the items inmy_list
.for index in indices
does the following:it creates a variable called
index
(the loop variable). the loop variable can have any name.the for loop goes through the list, element by element and in each step (“for each element”) it does the following:
set the value of the
indices
variable to the current element (in the first step, the curent element is the first element inindices
, in the second step, it is the second element inindices
etc.)execute the indented block (in this case, access the list element at
index
, and print the value ofitem
)
repeat this until the end of the list is reached
Clicker question “indentation”(Click me!)
Q: How often will “Test” be printed when running below code? Never? Once? 4 times? 5 times?
my_list = [10, 20, 30, 40, 50]
indices = [0, 1, 2, 3, 4]
for index in indices:
item = my_list[index]
print(item)
print('Test')
Note how we isolate the code that is repeated, and replace what changes with a variable. This makes the code more general. That way, we can apply our computation, like comparing a voltage to a threshold, for every list element.
Mini-Exercise: To illustrate how the for loop works, we can print not only the item but also the index used to access the item in each step of the loop. Modify the code above to do that.
Generating indices using range objects#
Above, we generate the list of indices by hand. This is a bit tedious if we have a long lists with thousands of elements, like a voltage recording. The range
function does that for you. In it’s simplest form, it takes an integer as an argument, and produces a range-object that can be used with a for loop to produces a series of integers:
indices = range(5) # this is equivalent to `[0, 1, 2, 3, 4]
for index in indices:
print("Item:", my_list[index])
Item: 10
Item: 20
Item: 30
Item: 40
Item: 50
Tip: It can be hard to learn about a range-object, since printing it does not return the indices it will generate. To do so, cast the range-object to a list and print it:
indices = range(5) # this is equivalent to `[0, 1, 2, 3, 4]
print(list(indices))
[0, 1, 2, 3, 4]
Note that range works a bit like indexing: It starts at zero, and ends before the argument. range(5)
produces integers starting at 0 and ending with 4. To generate a range-object with as many indices as the list we want to loop over, we can provide len(my_list)
as the argument to range. This will produce indices ending with the index of the last element of our list:
length_of_list = len(my_list)
indices = range(length_of_list)
for index in indices:
print("Item:", my_list[index])
Item: 10
Item: 20
Item: 30
Item: 40
Item: 50
Instead of simply printing the list elements, we can do any computation in the indented code block:
length_of_list = len(my_list)
indices = range(length_of_list)
for index in indices:
result = my_list[index] / 2
print("Result:", result)
Result: 5.0
Result: 10.0
Result: 15.0
Result: 20.0
Result: 25.0
Clicker question “range 1”(Click me!)
What list does list(range(3))
generate?
Clicker question “range 2”(Click me!)
Which range statement can be used to create the list [1, 3, 5]
?
Exercises “For loops” 1-4.
Generating more flexible ranges#
So far, we have learned to use the range function to generate range-objects, by specifying the end: range(stop)
will generate a range-object with indices starting at 0 and ending with stop-1.
However, there are two more ways to range:
Specifying not only the end but also the start:
range(start, stop)
Specifying the start, the end, and a step size:
range(start, stop, step)
You may have noticed that specifying a range works a bit like specifying slice indices.
range(5) # stop=5: [0, 1, 2, 3, 4]
range(2, 5) # start=2, stop=5: [2, 3, 4]
range(0, 5, 2); # start=0, stop=5, step=2: [0, 2, 4]
Exercise “Loops” 5-7.
An alternative way of using for loops#
Above, we have used range objects to generate indices to access list elements in a for loop.
my_list = [10, 20, 30, 40]
indices = [0, 1, 2, 3]
for index in indices:
item = my_list[index]
print(index, item)
0 10
1 20
2 30
3 40
Note above, that we access the elements of indices
, one after the other, in the for loop. We then use the indices, to access the elements of my_list
.
You may wonder: Can we access the elements of my_list
directly in the for loop? Yes, we can!
my_list = [10, 20, 30, 40]
for item in my_list:
print(item)
10
20
30
40
This is the standard way of accessing list elements using for loops in python. Range objects are typically used only if the index is needed for the computation. We will encounter these cases below. One example: When spike sorting, we want to compare each voltage value in our list of voltages to a threshold. In that case, we don’t care about the voltage value itself (=spike amplitude), but about the times (=indices) the threshold was crossed. In that case, you would need to use a range object. You can use either way of accessing list elements, using ranges or directly, whatever you find easier.
Different ways of looping over lists#
Directly over the elements#
So far, we have looped directly over list elements:
names = ['Tom', 'Yolanda', 'Estelle']
for name in names:
print(name)
Tom
Yolanda
Estelle
Using indices#
names = ['Tom', 'Yolanda', 'Estelle']
for index in range(len(names)): # loop over the indices
name = names[index] # get the current name using the index
print(index, name)
0 Tom
1 Yolanda
2 Estelle
Clicker question “for loops 1”(Click me!)
What is the output of this code?
my_list = [1,4,12]
for number in my_list:
print(number - 1)
Bonus: Nested loops#
For loops can be nested - indentation matters:
for outer in [10, 20, 30]:
print('outer', outer)
for inner in ['A', 'B', 'C']:
print(' inner', outer, inner)
outer 10
inner 10 A
inner 10 B
inner 10 C
outer 20
inner 20 A
inner 20 B
inner 20 C
outer 30
inner 30 A
inner 30 B
inner 30 C
Common code patterns#
Building lists#
So far we were able to manipulate list elements and print the result, but we did not have a way of saving the manipulated elements in a new list. You can do that by appending the results to a new empty list in the for loop:
# Goal: divide by ten each element in the list "data", collect the results in a new list "new_data"
data = [10, 20, 30]
print('data:', data)
# create an empty list that we will append the results to
new_data = []
# loop over all elements in data
for item in data:
result = item / 10 # divide each list element by 10
new_data.append(result) # append the result to the `new_data` list
print('results:', new_data)
data: [10, 20, 30]
results: [1.0, 2.0, 3.0]
Important: append
modifies the list in-place, it does not return a new list. This a = b.append(c)
DOES NOT WORK!
Filtering lists#
Append the results to a new list using a for loop if they match a criterion:
numbers = [1, 15, 3, 2, 11, 7, 20]
print('numbers:', numbers)
# create an empty list that we will append the results to
results = []
# loop over all elements in numbers
for number in numbers:
if number < 10: # if the current number is <10
results.append(number) # add the number to our results list
print(results)
numbers: [1, 15, 3, 2, 11, 7, 20]
[1, 3, 2, 7]
Clicker question “filtering”(Click me!)
What is the content of big_numbers at the end of this code?
all_numbers = [1,12,4,19,28]
big_numbers = []
for number in my_list:
if number > 10:
big_numbers.append(number)
print(big_numbers)
While loops#
For loops are great if you have a pre-specified list of items and you want to do something with each item. Or if you have a computation and want to run it a fixed amount of times. However, sometimes you do not know beforehand how often you need to run a computation or you have an unspecified amount of items you need to work through.
So-called while loops allow you to apply a computation as long as a specified condition is met. While loops have the following form:
while CONDITION:
do something
For instance, we can use a while loop to count down to zero:
count = 10
while count > 0: # apply the indented code as long as the value of `count` is greater than 0
print(count)
count = count - 1
print(count)
10
9
8
7
6
5
4
3
2
1
0
Bonus content#
List comprehensions#
We very often encounter code that applies a function to list elements and appends the results ina new list using a for loop:
numbers = [123, 423, 540]
results = []
for number in numbers:
results.append(number / 2)
List comprehensions are a way to write code like this more concisely:
results = [number / 2 for number in numbers]
Iterating over multiple lists in parallel using zip
#
If we want to iterate over two lists in parallel, we can use indices produced by range
to index into the different lists:
names = ['Tom', 'Ada', 'Jon']
ages = [14, 16, 12]
for index in range(len(names)):
print(names[index], ages[index])
Tom 14
Ada 16
Jon 12
zip
allows you to do that directly without having to use indices. Zip returns in each iteration a tuple of the individual list elemens:
names = ['Tom', 'Ada', 'Jon']
ages = [14, 16, 12]
for name, age in zip(names, ages):
print(name, age)
Tom 14
Ada 16
Jon 12
Getting list indices and list elements in a for loop using enumerate#
If you need both the list indices and the elements, you can again use range:
names = ['Tom', 'Ada', 'Jon']
for index in range(len(names)):
print(index, names[index])
0 Tom
1 Ada
2 Jon
enumerate
returns can be more concise, since it returns a tuple of the index and the element in each iteration:
names = ['Tom', 'Ada', 'Jon']
for index, name in enumerate(names):
print(index, name)
0 Tom
1 Ada
2 Jon
Spike detection using python#
Present data in code (individual voltage values, manipulate them and store the results) - variables
Compare variables (voltage to threshold) - boolean values
Perform different actions based on the value of a variable (only keep the position if the voltage exceeds the threshold) - if-else statements
Present and access data in a time series of voltage values - lists
Perform an action for each element in a sequence of values (inspect voltage values one-by-one) - for loops
Now you can write your spike detector!!
Next steps:
Separate data and logic so we can use the same code for new recordings - functions
Apply this to multi data files
Plot and save the results
Make everything more efficient and robust using numeric computation libraries (numpy, scipy)